#include <stdio.h>
#include <stdlib.h>

#include "log.h"

#include "fs/fbd.h"
#include "fs/isofs.h"
#include "fs/dirfs.h"

#include "hle.h"
#include "hle/__ModuleMgrForUser.h"
#include "hle/__IoFileMgrForUser.h"

#include "cpu/mips.h"

#include "loaders.h"

#include "core.h"

ISOFileSystem umd2iso;
DirFileSystem rootfsdir;

EmuFileType load_ident(const char *filename)
{
    char id[5];
    FILE *f = fopen(filename, "rb");
    if (!f) {
        /* File does not exists */
        _log(ERR, LOADER, "File %s does not exist", filename);
        return FILETYPE_ERROR;
    }
    if (fread(&id, 1, 4, f) != 4) {
        fclose(f);
        return FILETYPE_UNKNOWN;
    }
    fclose(f);

    if (memcmp(id, "\x7F""ELF", 4) == 0)
        return FILETYPE_ELF;
    else if (memcmp(id, "\x00PBP", 4) == 0)
        return FILETYPE_PBP;
    else if (strstr(filename, ".iso"))
        return FILETYPE_ISO;
    else if (memcmp(id, "~PSP", 4) == 0)
        _log(ERR, LOADER, "File is a PSP crypted file, impossible to decrypt !");

    _log(ERR, LOADER, "File not recognized!");
    return FILETYPE_UNKNOWN;
}

LoadResult load_execptr(char *ptr, char *filename)
{
    if (memcmp(ptr, "\x7F""ELF", 4) == 0)
    {
        _log(INF, LOADER, "Running ELF...");
        __KernelLoadExecFromPtr(ptr, filename);
        return LOAD_OK;
    }
    else if (memcmp(ptr, "~PSP", 4) == 0)
        _log(ERR, LOADER, "File is a PSP crypted file, impossible to decrypt!");
    else
        _log(ERR, LOADER, "File not recognized!");
    return LOAD_FAIL;
}

LoadResult load_rom(const char *filename)
{
    _log(INF, LOADER, "Loading file %s...", filename);
    switch (load_ident(filename)) 
    {
    case FILETYPE_PBP:
        return load_pbp(filename);

    case FILETYPE_ELF:
        return load_elf(filename);

    case FILETYPE_ISO:
        return load_iso(filename);

    default:
        _log(ERR, LOADER, "Can't load file");
        return LOAD_FAIL;
    }
    return LOAD_OK;
}

LoadResult load_raw(const char *filename, u32 addr)
{
    FILE *f = NULL;
    s32 c;
    u32 i;

    _log(INF, LOADER, "Loading raw file %s at %08x...", filename, addr);

    f = fopen(filename, "r");
    if (!f)
        return LOAD_FAIL;
        
    for (i = 0; (c = fgetc(f)) != EOF; i++)
        mem_write8(addr + i, c);

    fclose(f);

    cpu.pc = addr;
    return LOAD_OK;
}

LoadResult load_iso(const char *filename)
{
    char *paths[2] = {"disc0:/PSP_GAME/SYSDIR/BOOT.BIN", "disc0:/PSP_GAME/SYSDIR/EBOOT.BIN"};
    u32 id;
    char *ptr;
    u32 size;
    u8 i;

    System umd2;
    umd2.system = &umd2iso;

    _log(INF, LOADER, "Loading ISO...");

    if (!iso_test(filename)) {
        _log(ERR, LOADER, "ISO is invalid! Not loading");
        return LOAD_FAIL;
    }

    iso_init(&umd2iso, filename);

    mfs_mount("host0:", ISOFS, &umd2);
    mfs_mount("umd0:",  ISOFS, &umd2);
    mfs_mount("umd1:",  ISOFS, &umd2);
    mfs_mount("disc0:", ISOFS, &umd2);
    mfs_mount("UMD0:",  ISOFS, &umd2);
    mfs_mount("UMD1:",  ISOFS, &umd2);
    mfs_mount("DISC0:", ISOFS, &umd2);

    for (i = 0; i < 2; i++)
    {
        size = mfs_getFileInfo(paths[i]).size;
        if (size != 0)
        {
            ptr = malloc(size);
            id = mfs_openFile(paths[i], FILEACCESS_READ);
            mfs_readFile(id, ptr, size);
            if (load_execptr(ptr, paths[i]) == LOAD_OK) {
                free(ptr);
                return LOAD_OK;
            }
            free(ptr);
        }
    }

    return LOAD_FAIL;
}

LoadResult load_elf(const char *filename)
{
    char *total, *newfilename;
    LoadResult ret;

    System fs;
    SplittedPath path;
    fs.system = &rootfsdir;

    _log(INF, LOADER, "Loading ELF...");

    path = splitPath(filename);

    total = malloc(strlen(path.drive) + strlen(path.path) + 1);
    sprintf(total, "%s%s", path.drive, path.path);

    dfs_init(&rootfsdir);
    dfs_setBasePath(&rootfsdir, total);
    mfs_mount("umd0:", DIRFS, &fs);

    free(total);

    newfilename = malloc(strlen("umd0:/") + strlen(path.file) + strlen(path.ext) + 1);
    sprintf(newfilename, "umd0:/%s%s", path.file, path.ext);

    freeSplittedPath(&path);

    ret = __KernelLoadExec(newfilename);
    free(newfilename);
    return ret;
}

LoadResult load_pbp(const char *filename)
{
    FILE *infile;
    u32 offset6, offset7, size;
    char *elfcontent;
    char *path;
    LoadResult ret;
    s32 i, sz;

    _log(INF, LOADER, "Loading PBP...");

    /* Open the PBP file */
    infile = fopen(filename, "rb");

    /* Get the offsets of file 7 DATA.PSP and file 8 DATA.PSAR to get the size of DATA.PSP */
    fseek(infile, 32, SEEK_SET);
    if ((fread(&offset6, 1, 4, infile) != 4)
        || (fread(&offset7, 1, 4, infile) != 4)) {
        fclose(infile);
        return LOAD_FAIL;
    }

    /* Get the size of the file DATA.PSP */
    size = offset7 - offset6;
    /* Seek to the offset of this file */
    fseek(infile, offset6, SEEK_SET);
    if (!size) {
        fclose(infile);
        return LOAD_FAIL;
    }

    /* Copy DATA.PSP content to elfcontent */
    elfcontent = malloc(size);
    if (fread(elfcontent, 1, size, infile) != size) {
        free(elfcontent);
        fclose(infile);
        return LOAD_FAIL;
    }
   
    /* Close the pbp file */
    fclose(infile);

    /* Set the IO homebrew path name */
    for (i = strlen(filename) - 1; i > 0; i--)
        if (filename[i] == PTHSPRT)
            break;

    sz = i + 1;
    path = malloc(sz);

    for (i = 0; i < sz - 1; i++)
        path[i] = filename[i];
    path[i] = '\0';

    __IoSetPath(path);

    free(path);

    /* Executes the contained executable */
    ret = load_execptr(elfcontent, "ms0:/EBOOT.PBP");

    free(elfcontent);
   
    /* Exit successful */
    return ret;
}

PsfPair *load_psfParse(void *ptr)
{
    PsfHeader *header = ptr;
    PsfEntry *entptr = (PsfEntry*)((char*)ptr + sizeof(PsfHeader));
    PsfPair *pairs = malloc((header->num + 1) * sizeof(PsfPair));
    u32 i;
    char *entryptr;
    void *valueptr;
    for (i = 0; i < header->num; i++)
    {
        entryptr = (char*)ptr + header->keyoff + entptr[i].keyoff;
        pairs[i].entry = malloc(strlen(entryptr) + 1);
        pairs[i].entry[0] = 0;
        strcpy(pairs[i].entry, entryptr);

        valueptr = (void*)((char*)ptr + header->valueoff + entptr[i].valueoff);
        pairs[i].value = malloc(entptr[i].size);
        memcpy(pairs[i].value, valueptr, entptr[i].size);

        switch (entptr[i].type)
        {
        case 0:
            pairs[i].type = PSF_BIN;
            _log(INF, LOADER, "PSF: %s => (bin)", pairs[i].entry);
            break;

        case 2:
            pairs[i].type = PSF_TXT;
            _log(INF, LOADER, "PSF: %s => '%s'", pairs[i].entry, (char*)pairs[i].value);
            break;

        case 4:
            pairs[i].type = PSF_INT;
            _log(INF, LOADER, "PSF: %s => %d", pairs[i].entry, *(s32*)pairs[i].value);
            break;

        default:
            _log(ERR, LOADER, "Unknown data type in PSF!");
            break;
        }
    }
    pairs[i].type = PSF_END;
    return pairs;
}

void load_psfFree(PsfPair *pairs)
{
    u32 i;
    for (i = 0; pairs[i].type != PSF_END; i++) {
        free(pairs[i].entry);
        free(pairs[i].value);
    }
    free(pairs);
}

LoadResult load_getIsoInfo(const char *filename, IsoInfo *info, PsfPair **psfinfo)
{
    char *paths[2] = {"disc0:/PSP_GAME/SYSDIR/BOOT.BIN", "disc0:/PSP_GAME/SYSDIR/EBOOT.BIN"};
    u8 i;
    u8 foundexe = 0;

    System umd2;
    umd2.system = &umd2iso;

    if (!iso_test(filename)) {
        _log(INF, LOADER, "ISO is invalid! Not loading");
        return LOAD_FAIL;
    }
   
    iso_init(&umd2iso, filename);

    mfs_mount("disc0:", ISOFS, &umd2);

    for (i = 0; i < 2; i++)
        if (mfs_getFileInfo(paths[i]).size != 0)
            foundexe = 1;

    if (foundexe)
    {
        FileInfo file;
        u32 size;
        u32 handle;
        char *buf;
        char psfname[] = "disc0:/PSP_GAME/PARAM.SFO";
        u32 i;
        PsfPair *psf;

        /* Load PARAM.SFO to get game infos */
        file = mfs_getFileInfo(psfname);
        size = file.size;

        if (!size) {
            _log(ERR, HLE, "Can't load PARAM.SFO from ISO!");
            return LOAD_FAIL;
        }

        handle = mfs_openFile(psfname, FILEACCESS_READ);

        buf = malloc(sizeof(char) * size);
        mfs_readFile(handle, buf, (size_t)size);

        /* Load PSF info */
        psf = load_psfParse((void*)buf);
        for (i = 0; psf[i].type != PSF_END; i++)
        {
            if (strcmp(psf[i].entry, "TITLE") == 0)
                info->title = psf[i].value;
        }

        *psfinfo = psf;

        /* Close the file */
        free(buf);
        mfs_closeFile(handle);
    }

    mfs_umount("disc0:");

    return (foundexe ? LOAD_OK : LOAD_FAIL);
}

void load_findFilesInPath(IsoFileNode *firstnode, System *dfs, const char *path)
{
    FileInfo *listing;
    u32 n = dfs_getDirListing(dfs, path, &listing);
    u32 i;
    char *realpath, *fullpath;
    for (i = 0; i < n; i++)
    {
        fullpath = malloc(strlen(path) + strlen(listing[i].name) + 2);
        strcpy(fullpath, path);
        fullpath[strlen(path)] = PTHSPRT;
        fullpath[strlen(path) + 1] = '\0';
        strcat(fullpath, listing[i].name);
        if (listing[i].type == FILETYPE_DIRECTORY)
            load_findFilesInPath(firstnode, dfs, fullpath);
        else
        {
            IsoInfo info;
            PsfPair *pair;
            realpath = dfs_getRealPath(dfs, fullpath);
            if (load_getIsoInfo(realpath, &info, &pair) == LOAD_OK)
            {
                IsoFileNode *newnode;
                IsoFileNode *lastnode = firstnode;
                while (lastnode->next)
                    lastnode = lastnode->next;
                newnode = malloc(sizeof(IsoFileNode));
                newnode->pair = pair;
                newnode->info = info;
                newnode->filename = malloc(strlen(realpath) + 1);
                newnode->filename[0] = 0;
                strcpy(newnode->filename, realpath);
                newnode->next = NULL;
                newnode->prev = lastnode;
                lastnode->next = newnode;
            }
            dfs_freeRealPath(realpath);
        }
        free(fullpath);
    }
    dfs_freeDirListing(listing);
}

IsoFileNode load_findFiles(const char *path)
{
    System fs;
    DirFileSystem dfs;
    IsoFileNode root;
    dfs_init(&dfs);
    dfs_setBasePath(&dfs, "/");
    fs.system = &dfs;

    root.filename = NULL;
    root.next = NULL;
    root.prev = NULL;
    load_findFilesInPath(&root, &fs, path);

    dfs_free(&fs);

    return root;
}

void load_freeFiles(IsoFileNode *root)
{
    IsoFileNode *fn = root->next;
    if (!root->next)
        return;
    for (;;)
    {
        free(fn->filename);
        load_psfFree(fn->pair);
        if (fn->next)
            fn = fn->next;
        else {
            free(fn);
            break;
        }
        free(fn->prev);
    }
}

